<?php

namespace Library;

use Closure;
use Exception;
use mysqli;
use mysqli_result;
use mysqli_sql_exception;
use mysqli_stmt;

class QueryBuilder
{
    protected mysqli $conn;
    protected bool|mysqli_result $stmtResult = false;
    protected bool|mysqli_stmt $stmt = false;

    /**
     * @param array $config
     * @throws Exception
     */
    public function __construct(array $config){
        // 创建连接
        $this->conn = new mysqli(...$config);
        // 检查连接
        if ($this->conn->connect_error) {
            throw new Exception("连接失败: ". $this->conn->connect_error);
        }
    }

    /**
     * @param string $sql
     * @param Closure|null $closure
     * @return bool
     */
    public function multiQuery(string $sql, Closure $closure = null): bool
    {
        $result = $this->conn->multi_query($sql);
        if ($result && $closure!= null){
            do {
                $result = $this->conn->store_result();
                $closure($result);
                $result->free();
            } while ($this->conn->next_result()); // 移动到下一个结果集
        }else{
            if ($closure!= null) $closure(false);
        }
        return $result;
    }

    /**
     * @param string $sql
     * @param array|null $params
     * @param string $types
     * @return bool|mysqli_result
     */
    public function query(string $sql, array $params = null, string $types = ''): mysqli_result|bool
    {
        if(!$types) $types = str_repeat('s', count($params)); //String type for all variables if not specified
        if (version_compare(PHP_VERSION, '8.2', '>=')){
            $this->stmtResult = $this->conn->execute_query($sql, $params);
        }else{
            if ($params){
                $stmt = $this->conn->prepare($sql);
                $this->stmt = $stmt;
                $this->stmt->bind_param($types, ...$params);
                $this->stmt->execute();
                $this->stmtResult = $this->stmt->get_result();
            }else{
                $this->stmtResult = $this->conn->query($sql);
            }
        }
        return $this->stmtResult;
    }

    /**
     * Do stuff inside of transaction
     *
     * @param callable $callback Closure to do transaction operations inside. Parameter value is $this
     * @throws mysqli_sql_exception If transaction failed due to mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
     */
    public function transaction(callable $callback): void {
        try {
            $this->conn->autocommit(FALSE);
            $callback($this);
            $this->conn->autocommit(TRUE);
        } catch(mysqli_sql_exception $e) {
            $this->conn->rollback();
            throw $e;
        }
    }

    /**
     * @param $escape
     * @return $this
     */
    public function escape($escape): static
    {
        $this->conn->real_escape_string($escape);
        return $this;
    }

    /**
     * Get the latest primary key inserted
     *
     * @return int $mysqli->insert_id
     * @throws mysqli_sql_exception If mysqli function failed due to mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
     */
    public function insertId(): int {
        return $this->conn->insert_id;
    }

    /**
     * Frees MySQL result
     *
     * @throws mysqli_sql_exception If mysqli function failed due to mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
     * @return $this
     */
    public function freeResult(): self {
        if ($this->stmtResult instanceof mysqli_result){
            $this->stmtResult->free();
        }
        return $this;
    }

    /**
     * Get number of rows from SELECT
     *
     * @return int $mysqli->num_rows
     * @throws mysqli_sql_exception If mysqli function failed due to mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
     */
    public function numRows(): int {
        return $this->stmtResult->num_rows;
    }

    /**
     * Get affected rows. Can be used instead of numRows() in SELECT
     *
     * @return int $mysqli->affected_rows or rows matched if setRowsMatched() is used
     * @throws mysqli_sql_exception If mysqli function failed due to mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
     */
    public function affectedRows(): int {
        return $this->conn->affected_rows;
    }

    /**
     * A more specific version of affectedRows() to give you more info what happened. Uses $mysqli::info under the hood
     * Can be used for the following cases http://php.net/manual/en/mysqli.info.php
     *
     * @return array Associative array converted from result string
     * @throws mysqli_sql_exception If mysqli function failed due to mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
     */
    public function info(): array {
        preg_match_all('/(\S[^:]+): (\d+)/', $this->conn->info, $matches);
        return array_combine($matches[1], $matches[2]);
    }

    /**
     * Closes MySQL prepared statement
     *
     * @throws mysqli_sql_exception If mysqli function failed due to mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
     * @return $this
     */
    public function closeStmt(): self {
        if ($this->stmt instanceof mysqli_stmt){
            $this->stmt->close();
        }
        return $this;
    }

    /**
     * Closes MySQL connection
     *
     * @throws mysqli_sql_exception If mysqli function failed due to mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
     */
    public function close(): void {
        $this->conn->close();
    }

    public function __destruct(){
        $this->close();
    }
}